---
menu: Active Proposals
name: Invoker Commands (Explainer)
layout: ../../layouts/ComponentLayout.astro
---

- Authors: [Keith Cirkel](https://github.com/keithamus)

{/* START doctoc generated TOC please keep comment here to allow auto update */}
{/* DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE */}

# Invoker Buttons

## Pitch in Code

```html
<button command="showModal" commandfor="my-dialog">This opens a dialog</button>

<dialog id="my-dialog">This is the dialog</dialog>
```


## Introduction

Adding `commandfor` and `command` attributes to `<button>` and
`<input type="button">` / `<input type="reset">` elements would allow authors to
assign behaviour to buttons in a more accessible and declarative way, while
reducing bugs and simplifying the amount of JavaScript pages are required to
ship for interactivity. Buttons with `command` will - when clicked, touched, or
enacted via keypress - dispatch a `CommandEvent` on the element referenced by
`commandfor`, with some default behaviours.

## Background

All elements within the DOM are capable of having interactions added to them. A
long while ago this took the form of adding inline JavaScript to an event
attribute, such as `<button onclick="other.open()"></button>`. Inline JavaScript
has (rightly so) fallen out of favour due to the security and maintainability
concerns. Newer pages may instead introduce _more_ JavaScript to imperatively
discover elements and call `addEventListener('click', ...)` to invoke the same
behaviour. These patterns reduce developer experience and introduce more
boilerplate and friction, while remediating security and maintainability
concerns. Some frameworks attempt to reintroduce the developer experience of
inline handlers by introducing new JavaScript or HTML shorthands, such as
React's `onClick={...}`, Vue's `@click=".."` or HTMX's `hx-trigger="click"`.

Handling clicking is only half of the problem though, and this proposal doesn't
seek to re-invent click handlers. Dispatching an action to a related element on
the click can also become chore-like depending on the framework of choice. Many
frameworks recommend associating a click event with a state change and having
the element to be invoked react to that state change. For example [the React
docs on Adding Interactivity][1] attach click handlers which set state.


[1]: https://react.dev/learn/adding-interactivity#state-a-components-memory

There has also been a rise in the desire to customise controls for components.
Many sites, for example, introduce custom controls for File Uploads or dropdown
menus. These often require a large amount of work to _reintroduce_ the built in
functionality of those controls, and often unintentionally sacrifice
accessibility in doing so.

With the new `popover` attribute, we saw a straightforward declarative way to
tell the DOM that a button was interested in being a participant of the popover
interaction. `popovertarget` would indicate to a browser that if that button was
interacted with, it should open the element the `popovertarget` value pointed
to. This allows for popovers to be created and interacted with - in an
accessible and reliable way - without writing any additional JavaScript, which
is a very welcome addition. While `popovertarget` sufficiently captured the use
case for popovers, it fell short of providing the same developer & user
experience for other interactive elements such as `<dialog>`, `<details>`,
`<video>`, `<input type="file">`, and so on. This proposal attempts to redress
the balance.

### Terms

- Invoke/Invoked/Invoking: The action of _Invoking_ refers to a complete press
  action of a button, using a Human Input Device (HID). If the HID is a mouse,
  this would be a `click` event. If the HID is a touch screen, this would be a
  press using a stylus or finger. If the HID is a keyboard this would be the
  `Space` or `Enter` (Carriage Return) key on the keyboard. For other HIDs such
  as eye tracking or pedals or game controllers, the equivalent expected "click"
  action should be used to invoke the element.
- Invoker: An invoker is a button element (that is a `<button>`,
  `<input type="button">`, or `<input type="reset">`) that has an `commandfor`
  and `command` attribute set.
- Invoker Target: An element which is referenced to by an Invoker, via the
  `commandfor` attribute.

## Proposed Plan

In the style of `popovertarget`, this document proposes we add
`commandForElement`, and `command` as available attributes to `<button>`,
`<input type="button">` and `<input type="reset">` elements.

```webidl
interface mixin InvokerElement {
  [CEReactions] attribute Element? commandForElement;
  [CEReactions] attribute DOMString command;
};

HTMLButtonElement extends InvokerElement
HTMLInputElement extends InvokerElement
```

The `commandfor` value should be an IDREF pointing to an element within the
document. `.commandForElement` also exists on the element to imperatively
assign a node to be the invoker target, allowing for cross-root invokers (in
some cases, see
[the popovertarget attr-asociated element steps](https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#attr-associated-element)
for more).

The `command` (and the `.command` reflected property) is a freeform hint to the
invoker target. Command can be a "built-in" action or a "custom" action. If
`command` is not supplied then it is considered invalid and no action will take
place, and no event will be fired.

Custom command values _must_ contain a `-`. They will _never_ trigger a browser
default behaviour, aside from dispatching an `CommandEvent`. This allows web
developers to create custom Invoke actions for their components.

Built-in interactive elements have built-in behaviours (detailed below) which
are determined by the `command` attribue/property. The built-in names _must not_
contain a `-`. A `command` without a dash that is not a built-in is considered
invalid, and will not dispatch an CommandEvent.

Valid commands (that is: custom commands or a valid built-in) will
dispatch CommandEvents, allowing custom code to take control of invocations (for
example calling `.preventDefault()` or executing custom side-effects).

Invokers will dispatch an `CommandEvent` on the _Invokee_ (the element referenced
by `commandfor`) when the element is _Invoked_. The `CommandEvent`'s `type` is
always `command`. The event also contains an `invoker` property that will reference
the _Invoker_ element. `CommandEvents` are always non-bubbling, composed, trusted,
cancellable events.

```webidl
[Exposed=Window]
interface CommandEvent : Event {
  constructor(CommandEventInit invokeEventInit);
  readonly attribute Element invoker;
  readonly attribute DOMString type = "command";
  readonly attribute DOMString command;
};
dictionary CommandEventInit : EventInit {
  DOMString action = "";
  Element invoker;
};
```

If an element also has both a `popovertarget` and the `commandfor`/`command`
attributes set, then `popovertarget` _must_ be ignored: `command` takes
precedence.

If a `<button>` is a form participant, or has `type=submit`, then `command`
_must_ be ignored.

If an `<input>` is a form participant, or has a `type` other than `reset` or
`button`, then `command` _must_ be ignored.

### Example Code

#### Popovers

When pointing to a `popover`, `commandfor` acts much like `popovertarget`,
allowing the toggling of popovers.

```html
<button commandfor="my-popover" command="togglePopover">Open Popover</button>
<!-- Effectively the same as popovertarget="my-popover" -->

<div id="my-popover" popover="auto">Hello world</div>
```

#### Dialogs

When pointing to a `<dialog>`, `command` can toggle a `<dialog>`'s
openness.

```html
<button commandfor="my-dialog" command="showModal">Open Dialog</button>

<dialog id="my-dialog">
  Hello world!

  <button commandfor="my-dialog" command="close">Close</button>
</dialog>
```

#### Details

> Note: Invokers targeting a `<details>` element has been deferred from the initial release.

When pointing to a `<details>`, `commandfor` can toggle a `<details>`'
openness.

```html
<button commandfor="my-details" command="open">Open Details</button>
<!-- Can be used to replicate the `<summary>` interaction -->

<details id="my-details">
  <summary>Summary...</summary>
  Hello world!
</details>
```

#### Customizing `input type=file`

> Note: Invokers targeting a `<input>` element has been deferred from the initial release.

Pointing `commandfor` to an `<input type="file">` can act the same as the
rendered button _within_ the input; and can be used to declare a customised
alternative button to the input's button.

```html
<button commandfor="my-file" command="showPicker">Pick a file...</button>

<input id="my-file" type="file" />
```

#### Customizing video/audio controls

> Note: Invokers targeting `<audio>` and `<video>` elements has been deferred from the initial release.

The `<video>` and `<audio>` tags have many interactions, here `commandfor`
shines, allowing multiple buttons to handle different interactions with the
video player.

```html
<button commandfor="my-video" command="playpause">Play/Pause</button>
<button commandfor="my-video" command="toggleMuted">Mute/Unmute</button>

<video id="my-video"></video>
```

#### Custom behaviour

_Invokers_ will dispatch events on the _Invokee_ element. Using a dash in the
command allows for custom JavaScript to be triggered without having to wire up
manual event handlers to the Invokers.

```html
<!-- Custom commands must contain a dash! -->
<button commandfor="my-custom" command="my-frobulate">Frobulate</button>

<div id="my-custom"></div>

<script>
  document.getElementById("my-custom").addEventListener("command", (e) => {
    if (e.action === "my-frobulate") {
      alert("Successfully frobulated the div");
    }
  });
</script>
```

### Accessibility

> Warning: This section is incomplete PRs welcome.

The _Invoker_ implicitly receives `aria-controls=IDREF` or `aria-details=IDREF`
(tbd) to expose the _Invoker_ controls another element (the _Invokee_) for
instances where the invokee is not a sibling to the invoker element.

If the _Invokee_ has the `popover` attribute, the _Invoker_ implicitly receives
an `aria-expanded` state, as well as an `aria-details` association (for
instances where the elements are not DOM siblings) which will match the state of
the popover's openness. It will be `aria-expanded=true` when the `popover` is
`:popover-open` and `aria-expanded=false` otherwise.

If the _Invokee_ is a `<details>` element the _Invoker_ implicitly receives an
`aria-expanded` state which will match the state of the _Invokee_'s openness. It
will be `aria-expanded=true` when the _Invokee_ is open and
`aria-expanded=false` otherwise.

If the _Invokee_ is a `<dialog>` element the _Invoker_ implicitly receives an
`aria-expanded` state which will match the state of the _Invokee_'s openness. It
will be `aria-expanded=true` when the _Invokee_ is open and
`aria-expanded=false` otherwise.

### Defaults

Depending on the target set by `commandfor`, invoking the button will trigger
additional behaviours alongside the event dispatch, depending on the value of
`command`. The following table represents how built-in invocations on specific
element types are handled.

When the `command` attribute is missing it will default to an invalid state.

| Invokee Element | `action` hint         | Behaviour                                                                            |
| :-------------- | :-------------------- | :----------------------------------------------------------------------------------- |
| `<* popover>`   | `'togglePopover'`     | Call `.togglePopover()` on the invokee                                               |
| `<* popover>`   | `'hidePopover'`       | Call `.hidePopover()` on the invokee                                                 |
| `<* popover>`   | `'showPopover'`       | Call `.showPopover()` on the invokee                                                 |
| `<dialog>`      | `'showModal'`         | If the `<dialog>` is not `open`, call `showModal()`                                  |
| `<dialog>`      | `'close'`             | If the `<dialog>` is `open`, close and use the button `value` for returnValue        |

Further behaviours have been designed and may ship after the initial release of
Invokers, the names and exact semantics may be subject to change:

| Invokee Element | `action` hint         | Behaviour                                                                            |
| :-------------- | :-------------------- | :----------------------------------------------------------------------------------- |
| `<details>`     | `'toggle'`            | If the `<details>` is `open`, then close it, otherwise open it                       |
| `<details>`     | `'open'`              | If the `<details>` is not `open`, then open it                                       |
| `<details>`     | `'close'`             | If the `<details>` is `open`, then close it                                          |
| `<dialog>`      | `'toggle'`            | If the `<dialog>` is `open`, then close it and use the button `value` for returnValue, otherwise call `showModal()` |
| `<dialog>`      | `'cancel'`            | If the `<dialog>` is `open`, cancel the dialog                                       |
| `<select>`      | `'showPicker'`        | Call `.showPicker()` on the invokee                                                  |
| `<input>`       | `'showPicker'`        | Call `.showPicker()` on the invokee                                                  |
| `<video>`       | `'playpause'`         | Toggle the `.playing` value                                                          |
| `<video>`       | `'pause'`             | If `.playing` is `true`, set it to `false`                                           |
| `<video>`       | `'play'`              | If `.playing` is `false`, set it to `true`                                           |
| `<video>`       | `'toggleMuted'`       | Toggle the `.muted` value                                                            |
| `<audio>`       | `'playpause'`         | Toggle the `.playing` value                                                          |
| `<audio>`       | `'pause'`             | If `.playing` is `true`, set it to `false`                                           |
| `<audio>`       | `'play'`              | If `.playing` is `false`, set it to `true`                                           |
| `<audio>`       | `'toggleMuted'`       | Toggle the `.muted` value                                                            |
| `<*>`           | `'toggleFullscreen'`  | If the element is fullscreen, then exit, otherwise request to enter                  |
| `<*>`           | `'requestFullscreen'` | Request the element to enter into 'fullscreen' mode                                  |
| `<*>`           | `'exitFullscreen'`    | Request the element to exit 'fullscreen' mode                                        |
| `<input type=number>` | `'stepUp'`      | Call `.stepUp()` on the invokee                                                      |
| `<input type=number>` | `'stepDown'`    | Call `.stepDown()` on the invokee                                                    |

> Note: The above tables are an attempt at wide coverage, but ideas are welcome. Please submit a PR if you have one!

Further to the initial ship we're also exploring implicit `command` or implicit
`commandfor` values where the value can easily be inferred.

### Invokers and Custom Elements

As the underlying system for invoke elements uses event dispatch, Custom Elements
can make use of `CommandEvent` for their own behaviours. Consider the following:

```html
<button commandfor="my-element" command="spin-widget">
  Spin the widget
</button>

<spin-widget id="my-element"></spin-widget>
<script>
  customElements.define(
    "spin-widget",
    class extends HTMLElement {
      connectedCallback() {
        this.addEventListener("command", (e) => {
          if (e.action === "spin-widget") {
            this.spin();
          }
        });
      }
    },
  );
</script>
```

### PAQ (Potentially Asked Questions)

#### Why the name `command`? Why not `click`?

While `click` is a fairly well established name in the world of the web, it is
quite specific to the action from a user gesture, whereas invoke events notify
a related element that an interactive control is requesting action. In other
words, a `click` happens on the element being interacted with - the _invoker_,
while an `invoke` happens on the element the interactive control points to -
the _invokee_.

If the invokee were to recieve click events from the interactive control, this
would make it difficult to differentiate user interaction on the element vs on
invoking elements. Take for example a video player; clicking on the area which
renders the video may play/pause the video, but clicking on a video control may
cause different behaviours on the video. The extra controls _invoke an action
on the video_ but the user gesture was performed on the controls themselves.

Invokers _rely_ on click events for their behavior to work, and they are not
intended to replace them. Instead, invokers enhance click events to drive
behaviour on a different element. Consequently they need a new conceptual name
for this. Given the opportunity to supply a new name, `invoke` was settled on.

Given the new conceptual model of "command" it stands to reason that all related
concepts share the same name, as such `commandfor` and `command` were chosen
rather than, say, `clickfor`/`clickaction`. Similarly the event class is named
`CommandEvent` and the event name `command`.

#### Didn't this used to be called `invoke`? What happened?

The original proposal for Invoke Commands was to use the term `invoke`
exhaustively through the API, and so the attributes were originally named
`invoketarget`/`invokeaction` and the event named `InvokeEvent` with a type of
`invoke`. This was deemed too abstract, and standards groups and web developers
desired a simpler name. For those interested in the history, some conversation
that lead to changing `invoke` to `command` starts around here:
https://github.com/whatwg/html/issues/9625#issuecomment-2115718679

#### Do we have to always supply both? Can't we make `command` or `commandfor` implicit?

The original proposal had the concept of an "auto" `command` value which would
determine an exlicit command based on various heuristics, such as the target
element. This has been deferred for the initial ship, but may be explored
further. This is considred out of scope for the initial ship, however.

We may also explore the possibility of making `commandfor` implicit, for example
if a button is a descendant of a dialog, omitting `commandfor` may make sense.
This is also considered out of scope for the initial ship.

#### What about adding Invoker defaults for `<form>`?

Defaults for `<form>` are intentionally omitted as this proposal does not aim to
replace Reset or Submit buttons. If you want to control forms, use those.

#### What about adding Invoker defaults for `<a>`?

Defaults for `<a>` are intentionally omitted as this proposal does not aim to
replace anchors. If you intend to produce a page navigation, use an `<a>` tag.

### Why is `command`/`commandfor` limited to buttons?

This is by design, to allow for a "pit of success"; invoking actions on
non-button elements such as `<div>`s or `<a>`s creates many problems, especially
for non-interactive elements. While `<a>`s _are_ interactive, they should _only_
be used for page navigation and not for invoking other behaviours, and so
`command`/`commandfor` should not be allowed.

#### Why isn't `input[type=submit]` included?

This is not added by design. Submit inputs already have a default action:
submitting forms. If you want a button to control the submission of a form, use
`input[type=submit]`, if you want a button to control invocation of something
_other_ than a form then you should use `input[type=button]`.

#### Why _is_ `input[type=reset]` included?

It may stand to reason that if `input[type=submit]` is _excluded_ then so should
`input[type=reset]`, however, there are valid use cases to resetting a form at
the same time as some other action, for example closing the dialog that contains
a form:

```html
<dialog id="my-dialog">
  <form>
    <input type="text" />
    <!-- This button closes the dialog _and_ resets the form -->
    <input type="reset" commandfor="my-dialog" command="close" value="Cancel" />
  </form>
</dialog>
```

#### What does this mean for `popovertarget`?

Whilst `command`/`commandfor` _does_ replicate `popovertarget`'s functionality,
the two will co-exist side by side for some while to enable a smooth transition.

It is, however, intended that `commandfor` will replace `popovertarget` leading
to `popovertarget`'s eventual deprecation.

#### Command seems limited, what if I wanted to add arguments?

Custom `command`s can be used in a freeform way, for your own elements. If
you feel it necessary you can invent your own DSLs for passing extra data using
this hint. For example:

```html
<button commandfor="my-counter" command="add-1">Add 1</button>
<button commandfor="my-counter" command="add-2">Add 2</button>
<button commandfor="my-counter" command="add-10">Add 10</button>

<input readonly id="my-counter" value="0" />

<script>
  const counter = document.getElementById("my-counter");
  counter.addEventListener("command", (e) => {
    let addMatch = /^add-(\d+)$/.match(e.action);
    if (addMatch) {
      counter.value = Number(counter.value) + Number(addMatch[1]);
    }
  });
</script>
```

You can also leverage the buttons `value` attribute to effectively parameterise
certain commands:

```html
<button commandfor="my-counter" command="add-num" value="1">Add 1</button>
<button commandfor="my-counter" command="add-num" value="2">Add 2</button>
<button commandfor="my-counter" command="add-num" value="10">Add 10</button>

<input readonly id="my-counter" value="0" />

<script>
  const counter = document.getElementById("my-counter");
  counter.addEventListener("command", (e) => {
    if (e.command == 'add-num') {
      counter.value = Number(counter.value) + Number(e.invoker.value);
    }
  });
</script>
```
